package com.net.wheels.mybatis.generator.base;

import com.google.common.base.Strings;
import com.net.common.annotation.NonDomain;
import com.net.common.domain.CommonDomain;
import com.net.common.domain.VersionedDomain;
import com.net.common.enums.base.IntEnum;
import com.net.common.enums.base.StringEnum;
import com.net.common.util.NameUtil;
import io.swagger.annotations.ApiModelProperty;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;

import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;

import static org.reflections.ReflectionUtils.getAllMethods;
import static org.reflections.ReflectionUtils.withPrefix;

/**
 * Created by  on 2015/9/2.
 * 自动生成Mapper, Manager,Mapper.xml文件
 */
public class MapperGenerator {

    private Class<?> clazz;

    private String sqlPath;

    private String domainPath;

    private String shardName;

    private Boolean isId;

    private Map<String, String> fieldMap = new HashMap<>();

    private LinkedList<String> allNames = new LinkedList<>();

    //字段上的注释
    private Map<String, String> noteMap = new HashMap<>();

    public MapperGenerator(Class<?> clazz, String domainPath, String sqlPath, Boolean isId, String shardName) {
        this.clazz = clazz;
        this.domainPath = domainPath;
        this.sqlPath = sqlPath;
        this.isId = isId;
        this.shardName = shardName;
    }

    public void init() {
        getClassFields(clazz);
        Set<Method> methods = getAllMethods(clazz, withPrefix("get"), m -> m == null || !Modifier.isStatic(m.getModifiers()));
        List<Method> list = new ArrayList<>(methods);

        for (Method m : list) {
            if (m.getDeclaringClass().getSimpleName().equals("Object")) {
                continue;
            }
            String name = m.getName().substring(4);
            name = ("" + m.getName().charAt(3)).toLowerCase() + name;
            String type = getType(m.getReturnType());
            fieldMap.put(name, type);
        }

        allNames.addFirst("id");
        allNames.add("createTime");
        allNames.add("modifyTime");
    }

    private void getClassFields(Class clazz) {
        while ((clazz != Object.class && clazz != Serializable.class && clazz != CommonDomain.class)) {
            Field fs[] = clazz.getDeclaredFields(); // 取得当前类全部的变量     不包括接口的
            List<String> nameList = new ArrayList<>();
            for (Field f : fs) {
                if (!f.isAnnotationPresent(NonDomain.class)) {
                    nameList.add(f.getName());
                }

                ApiModelProperty annotation = f.getAnnotation(ApiModelProperty.class);

                if (annotation != null) {
                    String note = Strings.isNullOrEmpty(annotation.notes()) ? annotation.value() : annotation.notes();

                    if (!Strings.isNullOrEmpty(note)) {
                        noteMap.put(f.getName(), note);
                    }

                }
            }
            Collections.reverse(nameList);
            allNames.addAll(nameList);
            clazz = clazz.getSuperclass();
        }
        Collections.reverse(allNames);
    }

    public String generateTable() {
        StringBuilder buffer = new StringBuilder();
        buffer.append("DROP TABLE IF EXISTS `" + NameUtil.camelToUnderline(clazz.getSimpleName()) + "`;\n\n");
        buffer.append("CREATE TABLE `" + NameUtil.camelToUnderline(clazz.getSimpleName()) + "`(\n");
        for (String name : allNames) {
            String type = fieldMap.get(name);
            String line = getTableColumn(NameUtil.camelToUnderline(name), type);
            String note = noteMap.get(name);
            if (!Strings.isNullOrEmpty(note)) {
                line = line + " COMMENT '" + note + "'";
            }
            if (name.equalsIgnoreCase("id")) {
                if (isId) {
                    line = line + " NOT NULL AUTO_INCREMENT";
                } else {
                    line = line + " NOT NULL UNIQUE";
                }
            }
            if (name.equalsIgnoreCase("createTime") || name.equalsIgnoreCase("modifyTime")) {
                line = line + " NOT NULL";
            }
            line = line + ",\n";
            buffer.append(line);
        }
        String str = buffer.toString() + "PRIMARY KEY (id) \n";

        str = str + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
        return str;
    }

    public String generateSelect() {
        StringBuilder buffer = new StringBuilder();
        buffer.append("select ");
        LinkedList<String> sqlNames = replaceKeyName(allNames);
        int length = 1;
        for (int i = 0; i < sqlNames.size(); i++) {
            buffer.append(NameUtil.camelToUnderline(sqlNames.get(i)));
            if (i != sqlNames.size() - 1) {
                buffer.append(",");
            }
            if (buffer.length() > length * 90) {
                buffer.append("\n        ");
                length++;
            }
        }
        String str = buffer.toString();
        str += " from `" + NameUtil.camelToUnderline(clazz.getSimpleName()) + "`";
        return str;
    }

    public String generateInsert() {
        StringBuffer start = new StringBuffer();
        start.append("insert into `" + NameUtil.camelToUnderline(clazz.getSimpleName() + "`\n        ("));
        LinkedList<String> sqlNames = replaceKeyName(allNames);
        StringBuffer db = new StringBuffer();
        int length = 1;
        for (int i = 0; i < sqlNames.size(); i++) {
            if (sqlNames.get(i).equalsIgnoreCase("id") && isId) {
                continue;
            }
            db.append(NameUtil.camelToUnderline(sqlNames.get(i)));
            if (i != sqlNames.size() - 1) {
                db.append(",");
            }
            if (db.length() > length * 90) {
                db.append("\n        ");
                length++;
            }
        }
        String str = start.toString() + db.toString();
        str += ") \n        VALUES\n        (";

        StringBuffer values = new StringBuffer();
        length = 1;
        for (int i = 0; i < allNames.size(); i++) {
            String name = allNames.get(i);
            if (name.equalsIgnoreCase("id") && isId) {
                continue;
            }
            values.append("#{" + name + "}");
            if (i != allNames.size() - 1) {
                values.append(",");
            }
            if (values.length() > length * 90) {
                values.append("\n        ");
                length++;
            }
        }
        str += values.toString();
        str += ")";
        return str;
    }

    public String generateUpdate() {
        StringBuffer buffer = new StringBuffer();
        buffer.append("update `" + NameUtil.camelToUnderline(clazz.getSimpleName()) + "` set ");
        LinkedList<String> sqlNames = replaceKeyName(allNames);
        for (int i = 0; i < allNames.size(); i++) {
            if (allNames.get(i).equalsIgnoreCase("id") || allNames.get(i).equalsIgnoreCase("createTime")) {
                continue;
            }
            buffer.append("\n        " + NameUtil.camelToUnderline(sqlNames.get(i)) + "=" + "#{" + allNames.get(i) + "}");
            if (i != allNames.size() - 1) {
                buffer.append(",");
            }
        }
        String str = buffer.toString();
        str += "\n        where id=#{id}";
        return str;
    }

    public String generateQueryCondition() {
        StringBuffer buffer = new StringBuffer();
        buffer.append("            <if test=\"condition.id != null\">");
        buffer.append("\n                id = #{condition.id}");
        buffer.append("\n            </if>");
        LinkedList<String> sqlNames = replaceKeyName(allNames);
        for (int i = 0; i < allNames.size(); i++) {
            String name = allNames.get(i);
            if (name.equalsIgnoreCase("id") || name.equalsIgnoreCase("version") || name.equalsIgnoreCase("modifyTime") || name.equalsIgnoreCase("createTime")) {
                continue;
            }
            String type = fieldMap.get(name);
            if (type.equals("LocalDateTime")) {
                continue;//时间类型的不需要
            }

            buffer.append("\n            <if test=\"condition." + name + " != null\">");
            buffer.append("\n                AND " + NameUtil.camelToUnderline(sqlNames.get(i)) + "=" + "#{condition." + name + "}");
            buffer.append("\n            </if>");
        }

        return buffer.toString();
    }

    public String generateUpdateByPrimaryKeyWithVersion() {
        StringBuffer buffer = new StringBuffer();
        buffer.append("update " + NameUtil.camelToUnderline(clazz.getSimpleName()) + " set ");
        LinkedList<String> sqlNames = replaceKeyName(allNames);
        for (int i = 0; i < allNames.size(); i++) {
            if (allNames.get(i).equalsIgnoreCase("id") || allNames.get(i).equalsIgnoreCase("createTime")) {
                continue;
            }
            if (allNames.get(i).equalsIgnoreCase("version")) {
                buffer.append("\n        version=version + 1,");
                continue;
            }
            buffer.append("\n        " + NameUtil.camelToUnderline(sqlNames.get(i)) + "=" + "#{" + allNames.get(i) + "}");
            if (i != allNames.size() - 1) {
                buffer.append(",");
            }
        }
        String str = buffer.toString();
        str += "\n        where id=#{id} and version = #{version}";
        return str;
    }

    private String getType(Class<?> cl) {
        String name = cl.getSimpleName();
        if (name.equals("Integer")) {
            return "Integer";
        } else if (name.equals("Long")) {
            return "Long";
        } else if (name.equals("String")) {
            return "String";
        } else if (name.equals("Date")) {
            return "Date";
        } else if (name.equals("LocalDateTime")) {
            return "LocalDateTime";
        } else if (name.equals("LocalDate")) {
            return "LocalDate";
        } else if (name.equals("LocalTime")) {
            return "LocalTime";
        } else if (name.equals("Boolean")) {
            return "Integer";
        } else if (name.equals("BigDecimal")) {
            return "BigDecimal";
        } else if (name.equals("Byte[]")) {
            return "Byte[]";
        } else if (IntEnum.class.isAssignableFrom(cl)) {
            return "Integer";
        } else if (StringEnum.class.isAssignableFrom(cl)) {
            return "StringEnum";
        } else if (List.class.isAssignableFrom(cl)) {
            return "List";
        } else {
            return "Object";
        }
    }

    private String getTableColumn(String name, String type) {
        if (type.equals("Integer")) {
            return name + " int(20)";
        } else if (type.equals("Long")) {
            return name + " bigint(20)";
        } else if (type.equals("String")) {
            return name + " varchar(128)";
        } else if (type.equals("Date")) {
            return name + " datetime";
        } else if (type.equals("LocalDateTime")) {
            return name + " datetime";
        } else if (type.equals("LocalDate")) {
            return name + " date";
        } else if (type.equals("LocalTime")) {
            return name + " time";
        } else if (type.equals("BigDecimal")) {
            return name + " decimal(19,2)";
        } else if (type.equals("Byte[]")) {
            return name + " mediumblob";
        } else if (type.equals("StringEnum")) {
            return name + " varchar(64)";  //枚举类型的统一成64位的
        } else if (type.equals("List")) {
            return name + " varchar(1024)";  //枚举类型的统一成64位的
        } else {
            throw new RuntimeException("Unknown class name : " + type);
        }
    }

    private LinkedList<String> replaceKeyName(LinkedList<String> orderedNames) {
        LinkedList<String> sqlNames = new LinkedList<String>();
        int length = orderedNames.size();
        for (int i = 0; i < length; i++) {
            String sqlName = orderedNames.get(i);
            if (sqlName.equals("level")) {
                sqlName = "`level`";
            } else if (sqlName.equals("day")) {
                sqlName = "`day`";
            } else if (sqlName.equals("status")) {
                sqlName = "`status`";
            } else if (sqlName.equals("name")) {
                sqlName = "`name`";
            } else if (sqlName.equals("temporary")) {
                sqlName = "`temporary`";
            } else if (sqlName.equals("values")) {
                sqlName = "`values`";
            } else if (sqlName.equals("count")) {
                sqlName = "`count`";
            } else if (sqlName.equals("type")) {
                sqlName = "`type`";
            } else if (sqlName.equals("date")) {
                sqlName = "`date`";
            } else if (sqlName.equals("delayed")) {
                sqlName = "`delayed`";
            } else if (sqlName.equals("explain")) {
                sqlName = "`explain`";
            } else if (sqlName.equals("time")) {
                sqlName = "`time`";
            } else if (sqlName.equals("desc")) {
                sqlName = "`desc`";
            } else if (sqlName.equals("change")) {
                sqlName = "`change`";
            } else if (sqlName.equals("before")) {
                sqlName = "`before`";
            } else if (sqlName.equals("after")) {
                sqlName = "`after`";
            } else if (sqlName.equals("key")) {
                sqlName = "`key`";
            } else if (sqlName.equals("value")) {
                sqlName = "`value`";
            } else if (sqlName.equals("primary")) {
                sqlName= "`primary`";
            } else if (sqlName.equals("order")) {
                sqlName = "`order`";
            }
            sqlNames.add(sqlName);
        }
        return sqlNames;
    }

    private String toCreateFile(String fileName, String content, String path) {
        String res = "创建成功";
        File f = new File(path);
        if (!f.exists()) {
            f.mkdirs();
        }
        File file = new File(f, fileName);
        if (!file.exists()) {
            res = createFile(file, content, res);
        } else {
            file.delete();
            res = createFile(file, content, res);
        }
        return res;
    }

    private String createFile(File file, String content, String res) {
        try {
            file.createNewFile();
            FileWriter writer = new FileWriter(file, true);
            writer.write(content);
            writer.close();
        } catch (IOException e) {
            res = "创建失败";
            e.printStackTrace();
        }
        return res;
    }

    private String createMapperXML() {
        String ref;
        String tpltName = "mybtasi-template/XXXMapper.xml";
        if (isVersionedDomain()) {
            tpltName = "mybtasi-template/XXXVersionedMapper.xml";
        }
        VelocityContext context = new VelocityContext();
        context.put("namespace", getPath("dao") + "." + clazz.getSimpleName() + "Mapper");
        context.put("resultType", clazz.getSimpleName());
        context.put("table", "`" + NameUtil.camelToUnderline(clazz.getSimpleName()) + "`");
        context.put("select", generateSelect());
        context.put("insert", generateInsert());
        context.put("update", generateUpdate());
        context.put("queryCondition", generateQueryCondition());
        context.put("updateByPrimaryKeyWithVersion", generateUpdateByPrimaryKeyWithVersion());
        ref = replaceTpltContext(tpltName, context);
        return ref;
    }

    private String getPath(String replaceDomain) {
        String path = domainPath;
        if (path.contains("\\")) {
            path = path.substring(path.indexOf("com\\")).replace("\\", ".");
        } else {
            path = path.substring(path.indexOf("com/")).replace("/", ".");
        }
        path = path.replaceAll("domain", replaceDomain);
        return path;
    }

    private String createMapper() {
        String ref;
        String tpltName = "mybtasi-template/XXXMapper.java";
        if (isVersionedDomain()) {
            tpltName = "mybtasi-template/XXXVersionedMapper.java";
        }
        VelocityContext context = new VelocityContext();
        context.put("mapperPath", getPath("dao"));
        context.put("domainPath", getPath("domain") + "." + clazz.getSimpleName());
        context.put("namespace", clazz.getSimpleName());
        context.put("mapperName", clazz.getSimpleName() + "Mapper");
        ref = replaceTpltContext(tpltName, context);
        return ref;

    }

    private String createManager() {
        String ref;
        String tpltName = "mybtasi-template/XXXManager.java";
        if (isVersionedDomain()) {
            tpltName = "mybtasi-template/XXXVersionedManager.java";
        }
        VelocityContext context = new VelocityContext();
        context.put("managerPath", getPath("manager"));
        context.put("domainPath", getPath("domain") + "." + clazz.getSimpleName());
        context.put("namespace", clazz.getSimpleName());
        context.put("managerName", clazz.getSimpleName() + "Manager");
        ref = replaceTpltContext(tpltName, context);
        return ref;

    }

    private String createManagerImpl() {
        String ref;
        String absManager;
        String isShard;
        String tpltName = "mybtasi-template/XXXManagerImpl.java";
        if (isVersionedDomain()) {
            tpltName = "mybtasi-template/XXXVersionedManagerImpl.java";
        }
        VelocityContext context = new VelocityContext();
        context.put("managerImpl", getPath("manager\\.impl"));
        context.put("mapperPath", getPath("dao") + "." + clazz.getSimpleName() + "Mapper");
        context.put("domainPath", getPath("domain") + "." + clazz.getSimpleName());
        context.put("managerPath", getPath("manager") + "." + clazz.getSimpleName() + "Manager");
        context.put("managerName", clazz.getSimpleName() + "Manager");
        context.put("mapperName", clazz.getSimpleName() + "Mapper");
        context.put("namespace", clazz.getSimpleName());
        context.put("managerImplName", clazz.getSimpleName() + "ManagerImpl");

        isShard = "common.manager";
        absManager = "AbstractManager";

        if (isVersionedDomain()) {
            isShard = "common.manager";
            absManager = "AbstractVersionedManager";
        }
        context.put("isShard", isShard);
        context.put("absManager", absManager);
        ref = replaceTpltContext(tpltName, context);
        return ref;

    }

    private String replaceTpltContext(String tpltName, VelocityContext context) {
        String res = "";
        VelocityEngine engine = new VelocityEngine();
        engine.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
        engine.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
        Template template = engine.getTemplate(tpltName);
        Writer writer = new StringWriter();
        template.merge(context, writer);
        try {
            writer.flush();
            res = writer.toString();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return res;
    }

    private boolean isVersionedDomain() {
        return VersionedDomain.class.isAssignableFrom(clazz);
    }

    public void start() {
        System.out.println(getPath("mapper"));
        init();
        String res = toCreateFile(clazz.getSimpleName() + ".sql", generateTable(), sqlPath);
        System.out.println("SQL" + res);
        String res2 = toCreateFile(clazz.getSimpleName() + "Mapper.xml", createMapperXML(), domainPath.replaceAll("java", "resources").replaceAll("domain", "dao"));
        System.out.println("Mapper.xml" + res2);
        String res3 = toCreateFile(clazz.getSimpleName() + "Mapper.java", createMapper(), domainPath.replaceAll("domain", "dao"));
        System.out.println("Mapper.java" + res3);
        String res4 = toCreateFile(clazz.getSimpleName() + "Manager.java", createManager(), domainPath.replaceAll("domain", "manager"));
        System.out.println("Manager" + res4);
        String res5 = toCreateFile(clazz.getSimpleName() + "ManagerImpl.java", createManagerImpl(), domainPath.replaceAll("domain", "manager/impl"));
        System.out.println("ManagerImpl" + res5);
    }

}